//  SPDX-FileCopyrightText: Copyright (c) 2017 BalazsJako
//  SPDX-License-Identifier: MIT

// source: https://github.com/BalazsJako/ImGuiColorTextEdit

#include <algorithm>
#include <chrono>
#include <cmath>
#include <regex>
#include <string>

#include "text_editor.h"

#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui.h" // for imGui::GetCurrentWindow()

// TODO
// - multiline comments vs single-line: latter is blocking start of a ML

namespace Core::Devtools::Widget {

template <class InputIt1, class InputIt2, class BinaryPredicate>
bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p) {
    for (; first1 != last1 && first2 != last2; ++first1, ++first2) {
        if (!p(*first1, *first2))
            return false;
    }
    return first1 == last1 && first2 == last2;
}

TextEditor::TextEditor()
    : mLineSpacing(1.0f), mUndoIndex(0), mTabSize(4), mOverwrite(false), mReadOnly(false),
      mWithinRender(false), mScrollToCursor(false), mScrollToTop(false), mTextChanged(false),
      mColorizerEnabled(true), mTextStart(20.0f), mLeftMargin(10), mCursorPositionChanged(false),
      mColorRangeMin(0), mColorRangeMax(0), mSelectionMode(SelectionMode::Normal),
      mCheckComments(true), mLastClick(-1.0f), mHandleKeyboardInputs(true),
      mHandleMouseInputs(true), mIgnoreImGuiChild(false), mShowWhitespaces(true),
      mStartTime(std::chrono::duration_cast<std::chrono::milliseconds>(
                     std::chrono::system_clock::now().time_since_epoch())
                     .count()) {
    SetPalette(GetDarkPalette());
    mLines.push_back(Line());
}

TextEditor::~TextEditor() {}

void TextEditor::SetLanguageDefinition(const LanguageDefinition& aLanguageDef) {
    mLanguageDefinition = aLanguageDef;
    mRegexList.clear();

    for (auto& r : mLanguageDefinition.mTokenRegexStrings)
        mRegexList.push_back(
            std::make_pair(std::regex(r.first, std::regex_constants::optimize), r.second));

    Colorize();
}

void TextEditor::SetPalette(const Palette& aValue) {
    mPaletteBase = aValue;
}

std::string TextEditor::GetText(const Coordinates& aStart, const Coordinates& aEnd) const {
    std::string result;

    auto lstart = aStart.mLine;
    auto lend = aEnd.mLine;
    auto istart = GetCharacterIndex(aStart);
    auto iend = GetCharacterIndex(aEnd);
    size_t s = 0;

    for (size_t i = lstart; i < lend; i++)
        s += mLines[i].size();

    result.reserve(s + s / 8);

    while (istart < iend || lstart < lend) {
        if (lstart >= (int)mLines.size())
            break;

        auto& line = mLines[lstart];
        if (istart < (int)line.size()) {
            result += line[istart].mChar;
            istart++;
        } else {
            istart = 0;
            ++lstart;
            result += '\n';
        }
    }

    return result;
}

TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const {
    return SanitizeCoordinates(mState.mCursorPosition);
}

TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates& aValue) const {
    auto line = aValue.mLine;
    auto column = aValue.mColumn;
    if (line >= (int)mLines.size()) {
        if (mLines.empty()) {
            line = 0;
            column = 0;
        } else {
            line = (int)mLines.size() - 1;
            column = GetLineMaxColumn(line);
        }
        return Coordinates(line, column);
    } else {
        column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line));
        return Coordinates(line, column);
    }
}

// https://en.wikipedia.org/wiki/UTF-8
// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code
// sequence (non-10xxxxxx code)
static int UTF8CharLength(TextEditor::Char c) {
    if ((c & 0xFE) == 0xFC)
        return 6;
    if ((c & 0xFC) == 0xF8)
        return 5;
    if ((c & 0xF8) == 0xF0)
        return 4;
    else if ((c & 0xF0) == 0xE0)
        return 3;
    else if ((c & 0xE0) == 0xC0)
        return 2;
    return 1;
}

// "Borrowed" from ImGui source
static inline int ImTextCharToUtf8(char* buf, int buf_size, u32 c) {
    if (c < 0x80) {
        buf[0] = (char)c;
        return 1;
    }
    if (c < 0x800) {
        if (buf_size < 2)
            return 0;
        buf[0] = (char)(0xc0 + (c >> 6));
        buf[1] = (char)(0x80 + (c & 0x3f));
        return 2;
    }
    if (c >= 0xdc00 && c < 0xe000) {
        return 0;
    }
    if (c >= 0xd800 && c < 0xdc00) {
        if (buf_size < 4)
            return 0;
        buf[0] = (char)(0xf0 + (c >> 18));
        buf[1] = (char)(0x80 + ((c >> 12) & 0x3f));
        buf[2] = (char)(0x80 + ((c >> 6) & 0x3f));
        buf[3] = (char)(0x80 + ((c) & 0x3f));
        return 4;
    }
    // else if (c < 0x10000)
    {
        if (buf_size < 3)
            return 0;
        buf[0] = (char)(0xe0 + (c >> 12));
        buf[1] = (char)(0x80 + ((c >> 6) & 0x3f));
        buf[2] = (char)(0x80 + ((c) & 0x3f));
        return 3;
    }
}

void TextEditor::Advance(Coordinates& aCoordinates) const {
    if (aCoordinates.mLine < (int)mLines.size()) {
        auto& line = mLines[aCoordinates.mLine];
        auto cindex = GetCharacterIndex(aCoordinates);

        if (cindex + 1 < (int)line.size()) {
            auto delta = UTF8CharLength(line[cindex].mChar);
            cindex = std::min(cindex + delta, (int)line.size() - 1);
        } else {
            ++aCoordinates.mLine;
            cindex = 0;
        }
        aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex);
    }
}

void TextEditor::DeleteRange(const Coordinates& aStart, const Coordinates& aEnd) {
    ASSERT(aEnd >= aStart);
    ASSERT(!mReadOnly);

    // printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, aEnd.mColumn);

    if (aEnd == aStart)
        return;

    auto start = GetCharacterIndex(aStart);
    auto end = GetCharacterIndex(aEnd);

    if (aStart.mLine == aEnd.mLine) {
        auto& line = mLines[aStart.mLine];
        auto n = GetLineMaxColumn(aStart.mLine);
        if (aEnd.mColumn >= n)
            line.erase(line.begin() + start, line.end());
        else
            line.erase(line.begin() + start, line.begin() + end);
    } else {
        auto& firstLine = mLines[aStart.mLine];
        auto& lastLine = mLines[aEnd.mLine];

        firstLine.erase(firstLine.begin() + start, firstLine.end());
        lastLine.erase(lastLine.begin(), lastLine.begin() + end);

        if (aStart.mLine < aEnd.mLine)
            firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end());

        if (aStart.mLine < aEnd.mLine)
            RemoveLine(aStart.mLine + 1, aEnd.mLine + 1);
    }

    mTextChanged = true;
}

int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char* aValue) {
    ASSERT(!mReadOnly);

    int cindex = GetCharacterIndex(aWhere);
    int totalLines = 0;
    while (*aValue != '\0') {
        ASSERT(!mLines.empty());

        if (*aValue == '\r') {
            // skip
            ++aValue;
        } else if (*aValue == '\n') {
            if (cindex < (int)mLines[aWhere.mLine].size()) {
                auto& newLine = InsertLine(aWhere.mLine + 1);
                auto& line = mLines[aWhere.mLine];
                newLine.insert(newLine.begin(), line.begin() + cindex, line.end());
                line.erase(line.begin() + cindex, line.end());
            } else {
                InsertLine(aWhere.mLine + 1);
            }
            ++aWhere.mLine;
            aWhere.mColumn = 0;
            cindex = 0;
            ++totalLines;
            ++aValue;
        } else {
            auto& line = mLines[aWhere.mLine];
            auto d = UTF8CharLength(*aValue);
            while (d-- > 0 && *aValue != '\0')
                line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default));
            ++aWhere.mColumn;
        }

        mTextChanged = true;
    }

    return totalLines;
}

void TextEditor::AddUndo(UndoRecord& aValue) {
    ASSERT(!mReadOnly);
    // printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] (@%d.%d)\n",
    //	aValue.mBefore.mCursorPosition.mLine, aValue.mBefore.mCursorPosition.mColumn,
    //	aValue.mAdded.c_str(), aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn,
    // aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, 	aValue.mRemoved.c_str(),
    // aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, aValue.mRemovedEnd.mLine,
    // aValue.mRemovedEnd.mColumn, 	aValue.mAfter.mCursorPosition.mLine,
    // aValue.mAfter.mCursorPosition.mColumn
    //	);

    mUndoBuffer.resize((size_t)(mUndoIndex + 1));
    mUndoBuffer.back() = aValue;
    ++mUndoIndex;
}

TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition) const {
    ImVec2 origin = ImGui::GetCursorScreenPos();
    ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y);

    int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y));

    int columnCoord = 0;

    if (lineNo >= 0 && lineNo < (int)mLines.size()) {
        auto& line = mLines.at(lineNo);

        int columnIndex = 0;
        float columnX = 0.0f;

        while ((size_t)columnIndex < line.size()) {
            float columnWidth = 0.0f;

            if (line[columnIndex].mChar == '\t') {
                float spaceSize =
                    ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x;
                float oldX = columnX;
                float newColumnX =
                    (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) *
                    (float(mTabSize) * spaceSize);
                columnWidth = newColumnX - oldX;
                if (mTextStart + columnX + columnWidth * 0.5f > local.x)
                    break;
                columnX = newColumnX;
                columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize;
                columnIndex++;
            } else {
                char buf[7];
                auto d = UTF8CharLength(line[columnIndex].mChar);
                int i = 0;
                while (i < 6 && d-- > 0)
                    buf[i++] = line[columnIndex++].mChar;
                buf[i] = '\0';
                columnWidth =
                    ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf).x;
                if (mTextStart + columnX + columnWidth * 0.5f > local.x)
                    break;
                columnX += columnWidth;
                columnCoord++;
            }
        }
    }

    return SanitizeCoordinates(Coordinates(lineNo, columnCoord));
}

TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates& aFrom) const {
    Coordinates at = aFrom;
    if (at.mLine >= (int)mLines.size())
        return at;

    auto& line = mLines[at.mLine];
    auto cindex = GetCharacterIndex(at);

    if (cindex >= (int)line.size())
        return at;

    while (cindex > 0 && isspace(line[cindex].mChar))
        --cindex;

    auto cstart = (PaletteIndex)line[cindex].mColorIndex;
    while (cindex > 0) {
        auto c = line[cindex].mChar;
        if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx
        {
            if (c <= 32 && isspace(c)) {
                cindex++;
                break;
            }
            if (cstart != (PaletteIndex)line[size_t(cindex - 1)].mColorIndex)
                break;
        }
        --cindex;
    }
    return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex));
}

TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates& aFrom) const {
    Coordinates at = aFrom;
    if (at.mLine >= (int)mLines.size())
        return at;

    auto& line = mLines[at.mLine];
    auto cindex = GetCharacterIndex(at);

    if (cindex >= (int)line.size())
        return at;

    bool prevspace = (bool)isspace(line[cindex].mChar);
    auto cstart = (PaletteIndex)line[cindex].mColorIndex;
    while (cindex < (int)line.size()) {
        auto c = line[cindex].mChar;
        auto d = UTF8CharLength(c);
        if (cstart != (PaletteIndex)line[cindex].mColorIndex)
            break;

        if (prevspace != !!isspace(c)) {
            if (isspace(c))
                while (cindex < (int)line.size() && isspace(line[cindex].mChar))
                    ++cindex;
            break;
        }
        cindex += d;
    }
    return Coordinates(aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex));
}

TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates& aFrom) const {
    Coordinates at = aFrom;
    if (at.mLine >= (int)mLines.size())
        return at;

    // skip to the next non-word character
    auto cindex = GetCharacterIndex(aFrom);
    bool isword = false;
    bool skip = false;
    if (cindex < (int)mLines[at.mLine].size()) {
        auto& line = mLines[at.mLine];
        isword = isalnum(line[cindex].mChar);
        skip = isword;
    }

    while (!isword || skip) {
        if (at.mLine >= mLines.size()) {
            auto l = std::max(0, (int)mLines.size() - 1);
            return Coordinates(l, GetLineMaxColumn(l));
        }

        auto& line = mLines[at.mLine];
        if (cindex < (int)line.size()) {
            isword = isalnum(line[cindex].mChar);

            if (isword && !skip)
                return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex));

            if (!isword)
                skip = false;

            cindex++;
        } else {
            cindex = 0;
            ++at.mLine;
            skip = false;
            isword = false;
        }
    }

    return at;
}

int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const {
    if (aCoordinates.mLine >= mLines.size())
        return -1;
    auto& line = mLines[aCoordinates.mLine];
    int c = 0;
    int i = 0;
    for (; i < line.size() && c < aCoordinates.mColumn;) {
        if (line[i].mChar == '\t')
            c = (c / mTabSize) * mTabSize + mTabSize;
        else
            ++c;
        i += UTF8CharLength(line[i].mChar);
    }
    return i;
}

int TextEditor::GetCharacterColumn(int aLine, int aIndex) const {
    if (aLine >= mLines.size())
        return 0;
    auto& line = mLines[aLine];
    int col = 0;
    int i = 0;
    while (i < aIndex && i < (int)line.size()) {
        auto c = line[i].mChar;
        i += UTF8CharLength(c);
        if (c == '\t')
            col = (col / mTabSize) * mTabSize + mTabSize;
        else
            col++;
    }
    return col;
}

int TextEditor::GetLineCharacterCount(int aLine) const {
    if (aLine >= mLines.size())
        return 0;
    auto& line = mLines[aLine];
    int c = 0;
    for (unsigned i = 0; i < line.size(); c++)
        i += UTF8CharLength(line[i].mChar);
    return c;
}

int TextEditor::GetLineMaxColumn(int aLine) const {
    if (aLine >= mLines.size())
        return 0;
    auto& line = mLines[aLine];
    int col = 0;
    for (unsigned i = 0; i < line.size();) {
        auto c = line[i].mChar;
        if (c == '\t')
            col = (col / mTabSize) * mTabSize + mTabSize;
        else
            col++;
        i += UTF8CharLength(c);
    }
    return col;
}

bool TextEditor::IsOnWordBoundary(const Coordinates& aAt) const {
    if (aAt.mLine >= (int)mLines.size() || aAt.mColumn == 0)
        return true;

    auto& line = mLines[aAt.mLine];
    auto cindex = GetCharacterIndex(aAt);
    if (cindex >= (int)line.size())
        return true;

    if (mColorizerEnabled)
        return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex;

    return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar);
}

void TextEditor::RemoveLine(int aStart, int aEnd) {
    ASSERT(!mReadOnly);
    ASSERT(aEnd >= aStart);
    ASSERT(mLines.size() > (size_t)(aEnd - aStart));

    ErrorMarkers etmp;
    for (auto& i : mErrorMarkers) {
        ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second);
        if (e.first >= aStart && e.first <= aEnd)
            continue;
        etmp.insert(e);
    }
    mErrorMarkers = std::move(etmp);

    Breakpoints btmp;
    for (auto i : mBreakpoints) {
        if (i >= aStart && i <= aEnd)
            continue;
        btmp.insert(i >= aStart ? i - 1 : i);
    }
    mBreakpoints = std::move(btmp);

    mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd);
    ASSERT(!mLines.empty());

    mTextChanged = true;
}

void TextEditor::RemoveLine(int aIndex) {
    ASSERT(!mReadOnly);
    ASSERT(mLines.size() > 1);

    ErrorMarkers etmp;
    for (auto& i : mErrorMarkers) {
        ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second);
        if (e.first - 1 == aIndex)
            continue;
        etmp.insert(e);
    }
    mErrorMarkers = std::move(etmp);

    Breakpoints btmp;
    for (auto i : mBreakpoints) {
        if (i == aIndex)
            continue;
        btmp.insert(i >= aIndex ? i - 1 : i);
    }
    mBreakpoints = std::move(btmp);

    mLines.erase(mLines.begin() + aIndex);
    ASSERT(!mLines.empty());

    mTextChanged = true;
}

TextEditor::Line& TextEditor::InsertLine(int aIndex) {
    ASSERT(!mReadOnly);

    auto& result = *mLines.insert(mLines.begin() + aIndex, Line());

    ErrorMarkers etmp;
    for (auto& i : mErrorMarkers)
        etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second));
    mErrorMarkers = std::move(etmp);

    Breakpoints btmp;
    for (auto i : mBreakpoints)
        btmp.insert(i >= aIndex ? i + 1 : i);
    mBreakpoints = std::move(btmp);

    return result;
}

std::string TextEditor::GetWordUnderCursor() const {
    auto c = GetCursorPosition();
    return GetWordAt(c);
}

std::string TextEditor::GetWordAt(const Coordinates& aCoords) const {
    auto start = FindWordStart(aCoords);
    auto end = FindWordEnd(aCoords);

    std::string r;

    auto istart = GetCharacterIndex(start);
    auto iend = GetCharacterIndex(end);

    for (auto it = istart; it < iend; ++it)
        r.push_back(mLines[aCoords.mLine][it].mChar);

    return r;
}

ImU32 TextEditor::GetGlyphColor(const Glyph& aGlyph) const {
    if (!mColorizerEnabled)
        return mPalette[(int)PaletteIndex::Default];
    if (aGlyph.mComment)
        return mPalette[(int)PaletteIndex::Comment];
    if (aGlyph.mMultiLineComment)
        return mPalette[(int)PaletteIndex::MultiLineComment];
    auto const color = mPalette[(int)aGlyph.mColorIndex];
    if (aGlyph.mPreprocessor) {
        const auto ppcolor = mPalette[(int)PaletteIndex::Preprocessor];
        const int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2;
        const int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2;
        const int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2;
        const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2;
        return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24));
    }
    return color;
}

void TextEditor::HandleKeyboardInputs() {
    ImGuiIO& io = ImGui::GetIO();
    auto shift = io.KeyShift;
    auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl;
    auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt;

    if (ImGui::IsWindowFocused()) {
        if (ImGui::IsWindowHovered())
            ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
        // ImGui::CaptureKeyboardFromApp(true);

        io.WantCaptureKeyboard = true;
        io.WantTextInput = true;

        if (!IsReadOnly() && ctrl && !shift && !alt &&
            ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Z)))
            Undo();
        else if (!IsReadOnly() && !ctrl && !shift && alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace)))
            Undo();
        else if (!IsReadOnly() && ctrl && !shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Y)))
            Redo();
        else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow)))
            MoveUp(1, shift);
        else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow)))
            MoveDown(1, shift);
        else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow)))
            MoveLeft(1, shift, ctrl);
        else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow)))
            MoveRight(1, shift, ctrl);
        else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageUp)))
            MoveUp(GetPageSize() - 4, shift);
        else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageDown)))
            MoveDown(GetPageSize() - 4, shift);
        else if (!alt && ctrl && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home)))
            MoveTop(shift);
        else if (ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End)))
            MoveBottom(shift);
        else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home)))
            MoveHome(shift);
        else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End)))
            MoveEnd(shift);
        else if (!IsReadOnly() && !ctrl && !shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)))
            Delete();
        else if (!IsReadOnly() && !ctrl && !shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace)))
            Backspace();
        else if (!ctrl && !shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert)))
            mOverwrite ^= true;
        else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert)))
            Copy();
        else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C)))
            Copy();
        else if (!IsReadOnly() && !ctrl && shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert)))
            Paste();
        else if (!IsReadOnly() && ctrl && !shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V)))
            Paste();
        else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X)))
            Cut();
        else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)))
            Cut();
        else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_A)))
            SelectAll();
        else if (!IsReadOnly() && !ctrl && !shift && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)))
            EnterCharacter('\n', false);
        else if (!IsReadOnly() && !ctrl && !alt &&
                 ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab)))
            EnterCharacter('\t', shift);

        if (!IsReadOnly() && !io.InputQueueCharacters.empty()) {
            for (int i = 0; i < io.InputQueueCharacters.Size; i++) {
                auto c = io.InputQueueCharacters[i];
                if (c != 0 && (c == '\n' || c >= 32))
                    EnterCharacter(c, shift);
            }
            io.InputQueueCharacters.resize(0);
        }
    }
}

void TextEditor::HandleMouseInputs() {
    ImGuiIO& io = ImGui::GetIO();
    auto shift = io.KeyShift;
    auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl;
    auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt;

    if (ImGui::IsWindowHovered()) {
        if (!shift && !alt) {
            auto click = ImGui::IsMouseClicked(0);
            auto doubleClick = ImGui::IsMouseDoubleClicked(0);
            auto t = ImGui::GetTime();
            auto tripleClick = click && !doubleClick &&
                               (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime);

            /*
            Left mouse button triple click
            */

            if (tripleClick) {
                if (!ctrl) {
                    mState.mCursorPosition = mInteractiveStart = mInteractiveEnd =
                        ScreenPosToCoordinates(ImGui::GetMousePos());
                    mSelectionMode = SelectionMode::Line;
                    SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode);
                }

                mLastClick = -1.0f;
            }

            /*
            Left mouse button double click
            */

            else if (doubleClick) {
                if (!ctrl) {
                    mState.mCursorPosition = mInteractiveStart = mInteractiveEnd =
                        ScreenPosToCoordinates(ImGui::GetMousePos());
                    if (mSelectionMode == SelectionMode::Line)
                        mSelectionMode = SelectionMode::Normal;
                    else
                        mSelectionMode = SelectionMode::Word;
                    SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode);
                }

                mLastClick = (float)ImGui::GetTime();
            }

            /*
            Left mouse button click
            */
            else if (click) {
                mState.mCursorPosition = mInteractiveStart = mInteractiveEnd =
                    ScreenPosToCoordinates(ImGui::GetMousePos());
                if (ctrl)
                    mSelectionMode = SelectionMode::Word;
                else
                    mSelectionMode = SelectionMode::Normal;
                SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode);

                mLastClick = (float)ImGui::GetTime();
            }
            // Mouse left button dragging (=> update selection)
            else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) {
                io.WantCaptureMouse = true;
                mState.mCursorPosition = mInteractiveEnd =
                    ScreenPosToCoordinates(ImGui::GetMousePos());
                SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode);
            }
        }
    }
}

void TextEditor::Render() {
    /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/
    const float fontSize =
        ImGui::GetFont()
            ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr)
            .x;
    mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing);

    /* Update palette with the current alpha from style */
    for (int i = 0; i < (int)PaletteIndex::Max; ++i) {
        auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]);
        color.w *= ImGui::GetStyle().Alpha;
        mPalette[i] = ImGui::ColorConvertFloat4ToU32(color);
    }

    ASSERT(mLineBuffer.empty());

    auto contentSize = ImGui::GetWindowContentRegionMax();
    auto drawList = ImGui::GetWindowDrawList();
    float longest(mTextStart);

    if (mScrollToTop) {
        mScrollToTop = false;
        ImGui::SetScrollY(0.f);
    }

    ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos();
    auto scrollX = ImGui::GetScrollX();
    auto scrollY = ImGui::GetScrollY();

    auto lineNo = (int)floor(scrollY / mCharAdvance.y);
    auto globalLineMax = (int)mLines.size();
    auto lineMax =
        std::max(0, std::min((int)mLines.size() - 1,
                             lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y)));

    // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width
    char buf[16];
    snprintf(buf, 16, " %d ", globalLineMax);
    mTextStart = ImGui::GetFont()
                     ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr)
                     .x +
                 mLeftMargin;

    if (!mLines.empty()) {
        float spaceSize =
            ImGui::GetFont()
                ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr)
                .x;

        while (lineNo <= lineMax) {
            ImVec2 lineStartScreenPos =
                ImVec2(cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y);
            ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y);

            auto& line = mLines[lineNo];
            longest = std::max(
                mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))),
                longest);
            auto columnNo = 0;
            Coordinates lineStartCoord(lineNo, 0);
            Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo));

            // Draw selection for the current line
            float sstart = -1.0f;
            float ssend = -1.0f;

            ASSERT(mState.mSelectionStart <= mState.mSelectionEnd);
            if (mState.mSelectionStart <= lineEndCoord)
                sstart = mState.mSelectionStart > lineStartCoord
                             ? TextDistanceToLineStart(mState.mSelectionStart)
                             : 0.0f;
            if (mState.mSelectionEnd > lineStartCoord)
                ssend = TextDistanceToLineStart(
                    mState.mSelectionEnd < lineEndCoord ? mState.mSelectionEnd : lineEndCoord);

            if (mState.mSelectionEnd.mLine > lineNo)
                ssend += mCharAdvance.x;

            if (sstart != -1 && ssend != -1 && sstart < ssend) {
                ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y);
                ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend,
                            lineStartScreenPos.y + mCharAdvance.y);
                drawList->AddRectFilled(vstart, vend, mPalette[(int)PaletteIndex::Selection]);
            }

            // Draw breakpoints
            auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y);

            if (mBreakpoints.count(lineNo + 1) != 0) {
                auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX,
                                  lineStartScreenPos.y + mCharAdvance.y);
                drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::Breakpoint]);
            }

            // Draw error markers
            auto errorIt = mErrorMarkers.find(lineNo + 1);
            if (errorIt != mErrorMarkers.end()) {
                auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX,
                                  lineStartScreenPos.y + mCharAdvance.y);
                drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::ErrorMarker]);

                if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) {
                    ImGui::BeginTooltip();
                    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
                    ImGui::Text("Error at line %d:", errorIt->first);
                    ImGui::PopStyleColor();
                    ImGui::Separator();
                    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f));
                    ImGui::Text("%s", errorIt->second.c_str());
                    ImGui::PopStyleColor();
                    ImGui::EndTooltip();
                }
            }

            // Draw line number (right aligned)
            snprintf(buf, 16, "%d  ", lineNo + 1);

            auto lineNoWidth =
                ImGui::GetFont()
                    ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr)
                    .x;
            drawList->AddText(
                ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y),
                mPalette[(int)PaletteIndex::LineNumber], buf);

            if (mState.mCursorPosition.mLine == lineNo) {
                auto focused = ImGui::IsWindowFocused();

                // Highlight the current line (where the cursor is)
                if (!HasSelection()) {
                    auto end = ImVec2(start.x + contentSize.x + scrollX, start.y + mCharAdvance.y);
                    drawList->AddRectFilled(
                        start, end,
                        mPalette[(int)(focused ? PaletteIndex::CurrentLineFill
                                               : PaletteIndex::CurrentLineFillInactive)]);
                    drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge],
                                      1.0f);
                }

                // Render the cursor
                if (focused) {
                    auto timeEnd = std::chrono::duration_cast<std::chrono::milliseconds>(
                                       std::chrono::system_clock::now().time_since_epoch())
                                       .count();
                    auto elapsed = timeEnd - mStartTime;
                    if (elapsed > 400) {
                        float width = 1.0f;
                        auto cindex = GetCharacterIndex(mState.mCursorPosition);
                        float cx = TextDistanceToLineStart(mState.mCursorPosition);

                        if (mOverwrite && cindex < (int)line.size()) {
                            auto c = line[cindex].mChar;
                            if (c == '\t') {
                                auto x = (1.0f +
                                          std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) *
                                         (float(mTabSize) * spaceSize);
                                width = x - cx;
                            } else {
                                char buf2[2];
                                buf2[0] = line[cindex].mChar;
                                buf2[1] = '\0';
                                width =
                                    ImGui::GetFont()
                                        ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf2)
                                        .x;
                            }
                        }
                        ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y);
                        ImVec2 cend(textScreenPos.x + cx + width,
                                    lineStartScreenPos.y + mCharAdvance.y);
                        drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]);
                        if (elapsed > 800)
                            mStartTime = timeEnd;
                    }
                }
            }

            // Render colorized text
            auto prevColor =
                line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]);
            ImVec2 bufferOffset;

            for (int i = 0; i < line.size();) {
                auto& glyph = line[i];
                auto color = GetGlyphColor(glyph);

                if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') &&
                    !mLineBuffer.empty()) {
                    const ImVec2 newOffset(textScreenPos.x + bufferOffset.x,
                                           textScreenPos.y + bufferOffset.y);
                    drawList->AddText(newOffset, prevColor, mLineBuffer.c_str());
                    auto textSize =
                        ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f,
                                                        mLineBuffer.c_str(), nullptr, nullptr);
                    bufferOffset.x += textSize.x;
                    mLineBuffer.clear();
                }
                prevColor = color;

                if (glyph.mChar == '\t') {
                    auto oldX = bufferOffset.x;
                    bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) /
                                                        (float(mTabSize) * spaceSize))) *
                                     (float(mTabSize) * spaceSize);
                    ++i;

                    if (mShowWhitespaces) {
                        const auto s = ImGui::GetFontSize();
                        const auto x1 = textScreenPos.x + oldX + 1.0f;
                        const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f;
                        const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f;
                        const ImVec2 p1(x1, y);
                        const ImVec2 p2(x2, y);
                        const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f);
                        const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f);
                        drawList->AddLine(p1, p2, 0x90909090);
                        drawList->AddLine(p2, p3, 0x90909090);
                        drawList->AddLine(p2, p4, 0x90909090);
                    }
                } else if (glyph.mChar == ' ') {
                    if (mShowWhitespaces) {
                        const auto s = ImGui::GetFontSize();
                        const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f;
                        const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f;
                        drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4);
                    }
                    bufferOffset.x += spaceSize;
                    i++;
                } else {
                    auto l = UTF8CharLength(glyph.mChar);
                    while (l-- > 0)
                        mLineBuffer.push_back(line[i++].mChar);
                }
                ++columnNo;
            }

            if (!mLineBuffer.empty()) {
                const ImVec2 newOffset(textScreenPos.x + bufferOffset.x,
                                       textScreenPos.y + bufferOffset.y);
                drawList->AddText(newOffset, prevColor, mLineBuffer.c_str());
                mLineBuffer.clear();
            }

            ++lineNo;
        }

        // Draw a tooltip on known identifiers/preprocessor symbols
        if (ImGui::IsMousePosValid()) {
            auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos()));
            if (!id.empty()) {
                auto it = mLanguageDefinition.mIdentifiers.find(id);
                if (it != mLanguageDefinition.mIdentifiers.end()) {
                    ImGui::BeginTooltip();
                    ImGui::TextUnformatted(it->second.mDeclaration.c_str());
                    ImGui::EndTooltip();
                } else {
                    auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id);
                    if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) {
                        ImGui::BeginTooltip();
                        ImGui::TextUnformatted(pi->second.mDeclaration.c_str());
                        ImGui::EndTooltip();
                    }
                }
            }
        }
    }

    ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y));

    if (mScrollToCursor) {
        EnsureCursorVisible();
        ImGui::SetWindowFocus();
        mScrollToCursor = false;
    }
}

void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) {
    mWithinRender = true;
    mTextChanged = false;
    mCursorPositionChanged = false;

    ImGui::PushStyleColor(ImGuiCol_ChildBg,
                          ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background]));
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
    if (!mIgnoreImGuiChild)
        ImGui::BeginChild(aTitle, aSize, aBorder,
                          ImGuiWindowFlags_HorizontalScrollbar |
                              ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove |
                              ImGuiWindowFlags_NoNav);

    if (mHandleKeyboardInputs) {
        HandleKeyboardInputs();
        ImGui::PushAllowKeyboardFocus(true);
    }

    if (mHandleMouseInputs)
        HandleMouseInputs();

    ColorizeInternal();
    Render();

    if (mHandleKeyboardInputs)
        ImGui::PopAllowKeyboardFocus();

    if (!mIgnoreImGuiChild)
        ImGui::EndChild();

    ImGui::PopStyleVar();
    ImGui::PopStyleColor();

    mWithinRender = false;
}

void TextEditor::SetText(const std::string& aText) {
    mLines.clear();
    mLines.emplace_back(Line());
    for (auto chr : aText) {
        if (chr == '\r') {
            // ignore the carriage return character
        } else if (chr == '\n')
            mLines.emplace_back(Line());
        else {
            mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default));
        }
    }

    mTextChanged = true;
    mScrollToTop = true;

    mUndoBuffer.clear();
    mUndoIndex = 0;

    Colorize();
}

void TextEditor::SetTextLines(const std::vector<std::string>& aLines) {
    mLines.clear();

    if (aLines.empty()) {
        mLines.emplace_back(Line());
    } else {
        mLines.resize(aLines.size());

        for (size_t i = 0; i < aLines.size(); ++i) {
            const std::string& aLine = aLines[i];

            mLines[i].reserve(aLine.size());
            for (size_t j = 0; j < aLine.size(); ++j)
                mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default));
        }
    }

    mTextChanged = true;
    mScrollToTop = true;

    mUndoBuffer.clear();
    mUndoIndex = 0;

    Colorize();
}

void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) {
    ASSERT(!mReadOnly);

    UndoRecord u;

    u.mBefore = mState;

    if (HasSelection()) {
        if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) {

            auto start = mState.mSelectionStart;
            auto end = mState.mSelectionEnd;
            auto originalEnd = end;

            if (start > end)
                std::swap(start, end);
            start.mColumn = 0;
            //			end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0;
            if (end.mColumn == 0 && end.mLine > 0)
                --end.mLine;
            if (end.mLine >= (int)mLines.size())
                end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1;
            end.mColumn = GetLineMaxColumn(end.mLine);

            // if (end.mColumn >= GetLineMaxColumn(end.mLine))
            //	end.mColumn = GetLineMaxColumn(end.mLine) - 1;

            u.mRemovedStart = start;
            u.mRemovedEnd = end;
            u.mRemoved = GetText(start, end);

            bool modified = false;

            for (int i = start.mLine; i <= end.mLine; i++) {
                auto& line = mLines[i];
                if (aShift) {
                    if (!line.empty()) {
                        if (line.front().mChar == '\t') {
                            line.erase(line.begin());
                            modified = true;
                        } else {
                            for (int j = 0;
                                 j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) {
                                line.erase(line.begin());
                                modified = true;
                            }
                        }
                    }
                } else {
                    line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background));
                    modified = true;
                }
            }

            if (modified) {
                start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0));
                Coordinates rangeEnd;
                if (originalEnd.mColumn != 0) {
                    end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine));
                    rangeEnd = end;
                    u.mAdded = GetText(start, end);
                } else {
                    end = Coordinates(originalEnd.mLine, 0);
                    rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1));
                    u.mAdded = GetText(start, rangeEnd);
                }

                u.mAddedStart = start;
                u.mAddedEnd = rangeEnd;
                u.mAfter = mState;

                mState.mSelectionStart = start;
                mState.mSelectionEnd = end;
                AddUndo(u);

                mTextChanged = true;

                EnsureCursorVisible();
            }

            return;
        } // c == '\t'
        else {
            u.mRemoved = GetSelectedText();
            u.mRemovedStart = mState.mSelectionStart;
            u.mRemovedEnd = mState.mSelectionEnd;
            DeleteSelection();
        }
    } // HasSelection

    auto coord = GetActualCursorCoordinates();
    u.mAddedStart = coord;

    ASSERT(!mLines.empty());

    if (aChar == '\n') {
        InsertLine(coord.mLine + 1);
        auto& line = mLines[coord.mLine];
        auto& newLine = mLines[coord.mLine + 1];

        if (mLanguageDefinition.mAutoIndentation)
            for (size_t it = 0;
                 it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it)
                newLine.push_back(line[it]);

        const size_t whitespaceSize = newLine.size();
        auto cindex = GetCharacterIndex(coord);
        newLine.insert(newLine.end(), line.begin() + cindex, line.end());
        line.erase(line.begin() + cindex, line.begin() + line.size());
        SetCursorPosition(
            Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize)));
        u.mAdded = (char)aChar;
    } else {
        char buf[7];
        int e = ImTextCharToUtf8(buf, 7, aChar);
        if (e > 0) {
            buf[e] = '\0';
            auto& line = mLines[coord.mLine];
            auto cindex = GetCharacterIndex(coord);

            if (mOverwrite && cindex < (int)line.size()) {
                auto d = UTF8CharLength(line[cindex].mChar);

                u.mRemovedStart = mState.mCursorPosition;
                u.mRemovedEnd =
                    Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d));

                while (d-- > 0 && cindex < (int)line.size()) {
                    u.mRemoved += line[cindex].mChar;
                    line.erase(line.begin() + cindex);
                }
            }

            for (auto p = buf; *p != '\0'; p++, ++cindex)
                line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default));
            u.mAdded = buf;

            SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex)));
        } else
            return;
    }

    mTextChanged = true;

    u.mAddedEnd = GetActualCursorCoordinates();
    u.mAfter = mState;

    AddUndo(u);

    Colorize(coord.mLine - 1, 3);
    EnsureCursorVisible();
}

void TextEditor::SetReadOnly(bool aValue) {
    mReadOnly = aValue;
}

void TextEditor::SetColorizerEnable(bool aValue) {
    mColorizerEnabled = aValue;
}

void TextEditor::SetCursorPosition(const Coordinates& aPosition) {
    if (mState.mCursorPosition != aPosition) {
        mState.mCursorPosition = aPosition;
        mCursorPositionChanged = true;
        EnsureCursorVisible();
    }
}

void TextEditor::SetSelectionStart(const Coordinates& aPosition) {
    mState.mSelectionStart = SanitizeCoordinates(aPosition);
    if (mState.mSelectionStart > mState.mSelectionEnd)
        std::swap(mState.mSelectionStart, mState.mSelectionEnd);
}

void TextEditor::SetSelectionEnd(const Coordinates& aPosition) {
    mState.mSelectionEnd = SanitizeCoordinates(aPosition);
    if (mState.mSelectionStart > mState.mSelectionEnd)
        std::swap(mState.mSelectionStart, mState.mSelectionEnd);
}

void TextEditor::SetSelection(const Coordinates& aStart, const Coordinates& aEnd,
                              SelectionMode aMode) {
    auto oldSelStart = mState.mSelectionStart;
    auto oldSelEnd = mState.mSelectionEnd;

    mState.mSelectionStart = SanitizeCoordinates(aStart);
    mState.mSelectionEnd = SanitizeCoordinates(aEnd);
    if (mState.mSelectionStart > mState.mSelectionEnd)
        std::swap(mState.mSelectionStart, mState.mSelectionEnd);

    switch (aMode) {
    case TextEditor::SelectionMode::Normal:
        break;
    case TextEditor::SelectionMode::Word: {
        mState.mSelectionStart = FindWordStart(mState.mSelectionStart);
        if (!IsOnWordBoundary(mState.mSelectionEnd))
            mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd));
        break;
    }
    case TextEditor::SelectionMode::Line: {
        const auto lineNo = mState.mSelectionEnd.mLine;
        const auto lineSize = (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0;
        mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0);
        mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo));
        break;
    }
    default:
        break;
    }

    if (mState.mSelectionStart != oldSelStart || mState.mSelectionEnd != oldSelEnd)
        mCursorPositionChanged = true;
}

void TextEditor::SetTabSize(int aValue) {
    mTabSize = std::max(0, std::min(32, aValue));
}

void TextEditor::InsertText(const std::string& aValue) {
    InsertText(aValue.c_str());
}

void TextEditor::InsertText(const char* aValue) {
    if (aValue == nullptr)
        return;

    auto pos = GetActualCursorCoordinates();
    auto start = std::min(pos, mState.mSelectionStart);
    int totalLines = pos.mLine - start.mLine;

    totalLines += InsertTextAt(pos, aValue);

    SetSelection(pos, pos);
    SetCursorPosition(pos);
    Colorize(start.mLine - 1, totalLines + 2);
}

void TextEditor::DeleteSelection() {
    ASSERT(mState.mSelectionEnd >= mState.mSelectionStart);

    if (mState.mSelectionEnd == mState.mSelectionStart)
        return;

    DeleteRange(mState.mSelectionStart, mState.mSelectionEnd);

    SetSelection(mState.mSelectionStart, mState.mSelectionStart);
    SetCursorPosition(mState.mSelectionStart);
    Colorize(mState.mSelectionStart.mLine, 1);
}

void TextEditor::MoveUp(int aAmount, bool aSelect) {
    auto oldPos = mState.mCursorPosition;
    mState.mCursorPosition.mLine = std::max(0, mState.mCursorPosition.mLine - aAmount);
    if (oldPos != mState.mCursorPosition) {
        if (aSelect) {
            if (oldPos == mInteractiveStart)
                mInteractiveStart = mState.mCursorPosition;
            else if (oldPos == mInteractiveEnd)
                mInteractiveEnd = mState.mCursorPosition;
            else {
                mInteractiveStart = mState.mCursorPosition;
                mInteractiveEnd = oldPos;
            }
        } else
            mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
        SetSelection(mInteractiveStart, mInteractiveEnd);

        EnsureCursorVisible();
    }
}

void TextEditor::MoveDown(int aAmount, bool aSelect) {
    ASSERT(mState.mCursorPosition.mColumn >= 0);
    auto oldPos = mState.mCursorPosition;
    mState.mCursorPosition.mLine =
        std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + aAmount));

    if (mState.mCursorPosition != oldPos) {
        if (aSelect) {
            if (oldPos == mInteractiveEnd)
                mInteractiveEnd = mState.mCursorPosition;
            else if (oldPos == mInteractiveStart)
                mInteractiveStart = mState.mCursorPosition;
            else {
                mInteractiveStart = oldPos;
                mInteractiveEnd = mState.mCursorPosition;
            }
        } else
            mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
        SetSelection(mInteractiveStart, mInteractiveEnd);

        EnsureCursorVisible();
    }
}

static bool IsUTFSequence(char c) {
    return (c & 0xC0) == 0x80;
}

void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) {
    if (mLines.empty())
        return;

    auto oldPos = mState.mCursorPosition;
    mState.mCursorPosition = GetActualCursorCoordinates();
    auto line = mState.mCursorPosition.mLine;
    auto cindex = GetCharacterIndex(mState.mCursorPosition);

    while (aAmount-- > 0) {
        if (cindex == 0) {
            if (line > 0) {
                --line;
                if ((int)mLines.size() > line)
                    cindex = (int)mLines[line].size();
                else
                    cindex = 0;
            }
        } else {
            --cindex;
            if (cindex > 0) {
                if ((int)mLines.size() > line) {
                    while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar))
                        --cindex;
                }
            }
        }

        mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex));
        if (aWordMode) {
            mState.mCursorPosition = FindWordStart(mState.mCursorPosition);
            cindex = GetCharacterIndex(mState.mCursorPosition);
        }
    }

    mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex));

    ASSERT(mState.mCursorPosition.mColumn >= 0);
    if (aSelect) {
        if (oldPos == mInteractiveStart)
            mInteractiveStart = mState.mCursorPosition;
        else if (oldPos == mInteractiveEnd)
            mInteractiveEnd = mState.mCursorPosition;
        else {
            mInteractiveStart = mState.mCursorPosition;
            mInteractiveEnd = oldPos;
        }
    } else
        mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
    SetSelection(mInteractiveStart, mInteractiveEnd,
                 aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal);

    EnsureCursorVisible();
}

void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) {
    auto oldPos = mState.mCursorPosition;

    if (mLines.empty() || oldPos.mLine >= mLines.size())
        return;

    auto cindex = GetCharacterIndex(mState.mCursorPosition);
    while (aAmount-- > 0) {
        auto lindex = mState.mCursorPosition.mLine;
        auto& line = mLines[lindex];

        if (cindex >= line.size()) {
            if (mState.mCursorPosition.mLine < mLines.size() - 1) {
                mState.mCursorPosition.mLine =
                    std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1));
                mState.mCursorPosition.mColumn = 0;
            } else
                return;
        } else {
            cindex += UTF8CharLength(line[cindex].mChar);
            mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex));
            if (aWordMode)
                mState.mCursorPosition = FindNextWord(mState.mCursorPosition);
        }
    }

    if (aSelect) {
        if (oldPos == mInteractiveEnd)
            mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition);
        else if (oldPos == mInteractiveStart)
            mInteractiveStart = mState.mCursorPosition;
        else {
            mInteractiveStart = oldPos;
            mInteractiveEnd = mState.mCursorPosition;
        }
    } else
        mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
    SetSelection(mInteractiveStart, mInteractiveEnd,
                 aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal);

    EnsureCursorVisible();
}

void TextEditor::MoveTop(bool aSelect) {
    auto oldPos = mState.mCursorPosition;
    SetCursorPosition(Coordinates(0, 0));

    if (mState.mCursorPosition != oldPos) {
        if (aSelect) {
            mInteractiveEnd = oldPos;
            mInteractiveStart = mState.mCursorPosition;
        } else
            mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
        SetSelection(mInteractiveStart, mInteractiveEnd);
    }
}

void TextEditor::TextEditor::MoveBottom(bool aSelect) {
    auto oldPos = GetCursorPosition();
    auto newPos = Coordinates((int)mLines.size() - 1, 0);
    SetCursorPosition(newPos);
    if (aSelect) {
        mInteractiveStart = oldPos;
        mInteractiveEnd = newPos;
    } else
        mInteractiveStart = mInteractiveEnd = newPos;
    SetSelection(mInteractiveStart, mInteractiveEnd);
}

void TextEditor::MoveHome(bool aSelect) {
    auto oldPos = mState.mCursorPosition;
    SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0));

    if (mState.mCursorPosition != oldPos) {
        if (aSelect) {
            if (oldPos == mInteractiveStart)
                mInteractiveStart = mState.mCursorPosition;
            else if (oldPos == mInteractiveEnd)
                mInteractiveEnd = mState.mCursorPosition;
            else {
                mInteractiveStart = mState.mCursorPosition;
                mInteractiveEnd = oldPos;
            }
        } else
            mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
        SetSelection(mInteractiveStart, mInteractiveEnd);
    }
}

void TextEditor::MoveEnd(bool aSelect) {
    auto oldPos = mState.mCursorPosition;
    SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine)));

    if (mState.mCursorPosition != oldPos) {
        if (aSelect) {
            if (oldPos == mInteractiveEnd)
                mInteractiveEnd = mState.mCursorPosition;
            else if (oldPos == mInteractiveStart)
                mInteractiveStart = mState.mCursorPosition;
            else {
                mInteractiveStart = oldPos;
                mInteractiveEnd = mState.mCursorPosition;
            }
        } else
            mInteractiveStart = mInteractiveEnd = mState.mCursorPosition;
        SetSelection(mInteractiveStart, mInteractiveEnd);
    }
}

void TextEditor::Delete() {
    ASSERT(!mReadOnly);

    if (mLines.empty())
        return;

    UndoRecord u;
    u.mBefore = mState;

    if (HasSelection()) {
        u.mRemoved = GetSelectedText();
        u.mRemovedStart = mState.mSelectionStart;
        u.mRemovedEnd = mState.mSelectionEnd;

        DeleteSelection();
    } else {
        auto pos = GetActualCursorCoordinates();
        SetCursorPosition(pos);
        auto& line = mLines[pos.mLine];

        if (pos.mColumn == GetLineMaxColumn(pos.mLine)) {
            if (pos.mLine == (int)mLines.size() - 1)
                return;

            u.mRemoved = '\n';
            u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates();
            Advance(u.mRemovedEnd);

            auto& nextLine = mLines[pos.mLine + 1];
            line.insert(line.end(), nextLine.begin(), nextLine.end());
            RemoveLine(pos.mLine + 1);
        } else {
            auto cindex = GetCharacterIndex(pos);
            u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates();
            u.mRemovedEnd.mColumn++;
            u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd);

            auto d = UTF8CharLength(line[cindex].mChar);
            while (d-- > 0 && cindex < (int)line.size())
                line.erase(line.begin() + cindex);
        }

        mTextChanged = true;

        Colorize(pos.mLine, 1);
    }

    u.mAfter = mState;
    AddUndo(u);
}

void TextEditor::Backspace() {
    ASSERT(!mReadOnly);

    if (mLines.empty())
        return;

    UndoRecord u;
    u.mBefore = mState;

    if (HasSelection()) {
        u.mRemoved = GetSelectedText();
        u.mRemovedStart = mState.mSelectionStart;
        u.mRemovedEnd = mState.mSelectionEnd;

        DeleteSelection();
    } else {
        auto pos = GetActualCursorCoordinates();
        SetCursorPosition(pos);

        if (mState.mCursorPosition.mColumn == 0) {
            if (mState.mCursorPosition.mLine == 0)
                return;

            u.mRemoved = '\n';
            u.mRemovedStart = u.mRemovedEnd =
                Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1));
            Advance(u.mRemovedEnd);

            auto& line = mLines[mState.mCursorPosition.mLine];
            auto& prevLine = mLines[mState.mCursorPosition.mLine - 1];
            auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1);
            prevLine.insert(prevLine.end(), line.begin(), line.end());

            ErrorMarkers etmp;
            for (auto& i : mErrorMarkers)
                etmp.insert(ErrorMarkers::value_type(
                    i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second));
            mErrorMarkers = std::move(etmp);

            RemoveLine(mState.mCursorPosition.mLine);
            --mState.mCursorPosition.mLine;
            mState.mCursorPosition.mColumn = prevSize;
        } else {
            auto& line = mLines[mState.mCursorPosition.mLine];
            auto cindex = GetCharacterIndex(pos) - 1;
            auto cend = cindex + 1;
            while (cindex > 0 && IsUTFSequence(line[cindex].mChar))
                --cindex;

            // if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1)
            //	--cindex;

            u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates();
            --u.mRemovedStart.mColumn;
            --mState.mCursorPosition.mColumn;

            while (cindex < line.size() && cend-- > cindex) {
                u.mRemoved += line[cindex].mChar;
                line.erase(line.begin() + cindex);
            }
        }

        mTextChanged = true;

        EnsureCursorVisible();
        Colorize(mState.mCursorPosition.mLine, 1);
    }

    u.mAfter = mState;
    AddUndo(u);
}

void TextEditor::SelectWordUnderCursor() {
    auto c = GetCursorPosition();
    SetSelection(FindWordStart(c), FindWordEnd(c));
}

void TextEditor::SelectAll() {
    SetSelection(Coordinates(0, 0), Coordinates((int)mLines.size(), 0));
}

bool TextEditor::HasSelection() const {
    return mState.mSelectionEnd > mState.mSelectionStart;
}

void TextEditor::Copy() {
    if (HasSelection()) {
        ImGui::SetClipboardText(GetSelectedText().c_str());
    } else {
        if (!mLines.empty()) {
            std::string str;
            auto& line = mLines[GetActualCursorCoordinates().mLine];
            for (auto& g : line)
                str.push_back(g.mChar);
            ImGui::SetClipboardText(str.c_str());
        }
    }
}

void TextEditor::Cut() {
    if (IsReadOnly()) {
        Copy();
    } else {
        if (HasSelection()) {
            UndoRecord u;
            u.mBefore = mState;
            u.mRemoved = GetSelectedText();
            u.mRemovedStart = mState.mSelectionStart;
            u.mRemovedEnd = mState.mSelectionEnd;

            Copy();
            DeleteSelection();

            u.mAfter = mState;
            AddUndo(u);
        }
    }
}

void TextEditor::Paste() {
    if (IsReadOnly())
        return;

    auto clipText = ImGui::GetClipboardText();
    if (clipText != nullptr && strlen(clipText) > 0) {
        UndoRecord u;
        u.mBefore = mState;

        if (HasSelection()) {
            u.mRemoved = GetSelectedText();
            u.mRemovedStart = mState.mSelectionStart;
            u.mRemovedEnd = mState.mSelectionEnd;
            DeleteSelection();
        }

        u.mAdded = clipText;
        u.mAddedStart = GetActualCursorCoordinates();

        InsertText(clipText);

        u.mAddedEnd = GetActualCursorCoordinates();
        u.mAfter = mState;
        AddUndo(u);
    }
}

bool TextEditor::CanUndo() const {
    return !mReadOnly && mUndoIndex > 0;
}

bool TextEditor::CanRedo() const {
    return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size();
}

void TextEditor::Undo(int aSteps) {
    while (CanUndo() && aSteps-- > 0)
        mUndoBuffer[--mUndoIndex].Undo(this);
}

void TextEditor::Redo(int aSteps) {
    while (CanRedo() && aSteps-- > 0)
        mUndoBuffer[mUndoIndex++].Redo(this);
}

const TextEditor::Palette& TextEditor::GetDarkPalette() {
    const static Palette p = {{
        0xff7f7f7f, // Default
        0xffd69c56, // Keyword
        0xff00ff00, // Number
        0xff7070e0, // String
        0xff70a0e0, // Char literal
        0xffffffff, // Punctuation
        0xff408080, // Preprocessor
        0xffaaaaaa, // Identifier
        0xff9bc64d, // Known identifier
        0xffc040a0, // Preproc identifier
        0xff206020, // Comment (single line)
        0xff406020, // Comment (multi line)
        0xff101010, // Background
        0xffe0e0e0, // Cursor
        0x80a06020, // Selection
        0x800020ff, // ErrorMarker
        0x40f08000, // Breakpoint
        0xff707000, // Line number
        0x40000000, // Current line fill
        0x40808080, // Current line fill (inactive)
        0x40a0a0a0, // Current line edge
    }};
    return p;
}

const TextEditor::Palette& TextEditor::GetLightPalette() {
    const static Palette p = {{
        0xff7f7f7f, // None
        0xffff0c06, // Keyword
        0xff008000, // Number
        0xff2020a0, // String
        0xff304070, // Char literal
        0xff000000, // Punctuation
        0xff406060, // Preprocessor
        0xff404040, // Identifier
        0xff606010, // Known identifier
        0xffc040a0, // Preproc identifier
        0xff205020, // Comment (single line)
        0xff405020, // Comment (multi line)
        0xffffffff, // Background
        0xff000000, // Cursor
        0x80600000, // Selection
        0xa00010ff, // ErrorMarker
        0x80f08000, // Breakpoint
        0xff505000, // Line number
        0x40000000, // Current line fill
        0x40808080, // Current line fill (inactive)
        0x40000000, // Current line edge
    }};
    return p;
}

const TextEditor::Palette& TextEditor::GetRetroBluePalette() {
    const static Palette p = {{
        0xff00ffff, // None
        0xffffff00, // Keyword
        0xff00ff00, // Number
        0xff808000, // String
        0xff808000, // Char literal
        0xffffffff, // Punctuation
        0xff008000, // Preprocessor
        0xff00ffff, // Identifier
        0xffffffff, // Known identifier
        0xffff00ff, // Preproc identifier
        0xff808080, // Comment (single line)
        0xff404040, // Comment (multi line)
        0xff800000, // Background
        0xff0080ff, // Cursor
        0x80ffff00, // Selection
        0xa00000ff, // ErrorMarker
        0x80ff8000, // Breakpoint
        0xff808000, // Line number
        0x40000000, // Current line fill
        0x40808080, // Current line fill (inactive)
        0x40000000, // Current line edge
    }};
    return p;
}

std::string TextEditor::GetText() const {
    return GetText(Coordinates(), Coordinates((int)mLines.size(), 0));
}

std::vector<std::string> TextEditor::GetTextLines() const {
    std::vector<std::string> result;

    result.reserve(mLines.size());

    for (auto& line : mLines) {
        std::string text;

        text.resize(line.size());

        for (size_t i = 0; i < line.size(); ++i)
            text[i] = line[i].mChar;

        result.emplace_back(std::move(text));
    }

    return result;
}

std::string TextEditor::GetSelectedText() const {
    return GetText(mState.mSelectionStart, mState.mSelectionEnd);
}

std::string TextEditor::GetCurrentLineText() const {
    auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine);
    return GetText(Coordinates(mState.mCursorPosition.mLine, 0),
                   Coordinates(mState.mCursorPosition.mLine, lineLength));
}

void TextEditor::ProcessInputs() {}

void TextEditor::Colorize(int aFromLine, int aLines) {
    int toLine =
        aLines == -1 ? (int)mLines.size() : std::min((int)mLines.size(), aFromLine + aLines);
    mColorRangeMin = std::min(mColorRangeMin, aFromLine);
    mColorRangeMax = std::max(mColorRangeMax, toLine);
    mColorRangeMin = std::max(0, mColorRangeMin);
    mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax);
    mCheckComments = true;
}

void TextEditor::ColorizeRange(int aFromLine, int aToLine) {
    if (mLines.empty() || aFromLine >= aToLine)
        return;

    std::string buffer;
    std::cmatch results;
    std::string id;

    int endLine = std::max(0, std::min((int)mLines.size(), aToLine));
    for (int i = aFromLine; i < endLine; ++i) {
        auto& line = mLines[i];

        if (line.empty())
            continue;

        buffer.resize(line.size());
        for (size_t j = 0; j < line.size(); ++j) {
            auto& col = line[j];
            buffer[j] = col.mChar;
            col.mColorIndex = PaletteIndex::Default;
        }

        const char* bufferBegin = &buffer.front();
        const char* bufferEnd = bufferBegin + buffer.size();

        auto last = bufferEnd;

        for (auto first = bufferBegin; first != last;) {
            const char* token_begin = nullptr;
            const char* token_end = nullptr;
            PaletteIndex token_color = PaletteIndex::Default;

            bool hasTokenizeResult = false;

            if (mLanguageDefinition.mTokenize != nullptr) {
                if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, token_color))
                    hasTokenizeResult = true;
            }

            if (hasTokenizeResult == false) {
                // todo : remove
                // printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first),
                // first);

                for (auto& p : mRegexList) {
                    if (std::regex_search(first, last, results, p.first,
                                          std::regex_constants::match_continuous)) {
                        hasTokenizeResult = true;

                        auto& v = *results.begin();
                        token_begin = v.first;
                        token_end = v.second;
                        token_color = p.second;
                        break;
                    }
                }
            }

            if (hasTokenizeResult == false) {
                first++;
            } else {
                const size_t token_length = token_end - token_begin;

                if (token_color == PaletteIndex::Identifier) {
                    id.assign(token_begin, token_end);

                    // todo : allmost all language definitions use lower case to specify keywords,
                    // so shouldn't this use ::tolower ?
                    if (!mLanguageDefinition.mCaseSensitive)
                        std::transform(id.begin(), id.end(), id.begin(), ::toupper);

                    if (!line[first - bufferBegin].mPreprocessor) {
                        if (mLanguageDefinition.mKeywords.count(id) != 0)
                            token_color = PaletteIndex::Keyword;
                        else if (mLanguageDefinition.mIdentifiers.count(id) != 0)
                            token_color = PaletteIndex::KnownIdentifier;
                        else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0)
                            token_color = PaletteIndex::PreprocIdentifier;
                    } else {
                        if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0)
                            token_color = PaletteIndex::PreprocIdentifier;
                    }
                }

                for (size_t j = 0; j < token_length; ++j)
                    line[(token_begin - bufferBegin) + j].mColorIndex = token_color;

                first = token_end;
            }
        }
    }
}

void TextEditor::ColorizeInternal() {
    if (mLines.empty() || !mColorizerEnabled)
        return;

    if (mCheckComments) {
        auto endLine = mLines.size();
        auto endIndex = 0;
        auto commentStartLine = endLine;
        auto commentStartIndex = endIndex;
        auto withinString = false;
        auto withinSingleLineComment = false;
        auto withinPreproc = false;
        auto firstChar = true;    // there is no other non-whitespace characters in the line before
        auto concatenate = false; // '\' on the very end of the line
        auto currentLine = 0;
        auto currentIndex = 0;
        while (currentLine < endLine || currentIndex < endIndex) {
            auto& line = mLines[currentLine];

            if (currentIndex == 0 && !concatenate) {
                withinSingleLineComment = false;
                withinPreproc = false;
                firstChar = true;
            }

            concatenate = false;

            if (!line.empty()) {
                auto& g = line[currentIndex];
                auto c = g.mChar;

                if (c != mLanguageDefinition.mPreprocChar && !isspace(c))
                    firstChar = false;

                if (currentIndex == (int)line.size() - 1 && line[line.size() - 1].mChar == '\\')
                    concatenate = true;

                bool inComment =
                    (commentStartLine < currentLine ||
                     (commentStartLine == currentLine && commentStartIndex <= currentIndex));

                if (withinString) {
                    line[currentIndex].mMultiLineComment = inComment;

                    if (c == '\"') {
                        if (currentIndex + 1 < (int)line.size() &&
                            line[currentIndex + 1].mChar == '\"') {
                            currentIndex += 1;
                            if (currentIndex < (int)line.size())
                                line[currentIndex].mMultiLineComment = inComment;
                        } else
                            withinString = false;
                    } else if (c == '\\') {
                        currentIndex += 1;
                        if (currentIndex < (int)line.size())
                            line[currentIndex].mMultiLineComment = inComment;
                    }
                } else {
                    if (firstChar && c == mLanguageDefinition.mPreprocChar)
                        withinPreproc = true;

                    if (c == '\"') {
                        withinString = true;
                        line[currentIndex].mMultiLineComment = inComment;
                    } else {
                        auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; };
                        auto from = line.begin() + currentIndex;
                        auto& startStr = mLanguageDefinition.mCommentStart;
                        auto& singleStartStr = mLanguageDefinition.mSingleLineComment;

                        if (singleStartStr.size() > 0 &&
                            currentIndex + singleStartStr.size() <= line.size() &&
                            equals(singleStartStr.begin(), singleStartStr.end(), from,
                                   from + singleStartStr.size(), pred)) {
                            withinSingleLineComment = true;
                        } else if (!withinSingleLineComment &&
                                   currentIndex + startStr.size() <= line.size() &&
                                   equals(startStr.begin(), startStr.end(), from,
                                          from + startStr.size(), pred)) {
                            commentStartLine = currentLine;
                            commentStartIndex = currentIndex;
                        }

                        inComment = inComment =
                            (commentStartLine < currentLine || (commentStartLine == currentLine &&
                                                                commentStartIndex <= currentIndex));

                        line[currentIndex].mMultiLineComment = inComment;
                        line[currentIndex].mComment = withinSingleLineComment;

                        auto& endStr = mLanguageDefinition.mCommentEnd;
                        if (currentIndex + 1 >= (int)endStr.size() &&
                            equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1,
                                   pred)) {
                            commentStartIndex = endIndex;
                            commentStartLine = endLine;
                        }
                    }
                }
                line[currentIndex].mPreprocessor = withinPreproc;
                currentIndex += UTF8CharLength(c);
                if (currentIndex >= (int)line.size()) {
                    currentIndex = 0;
                    ++currentLine;
                }
            } else {
                currentIndex = 0;
                ++currentLine;
            }
        }
        mCheckComments = false;
    }

    if (mColorRangeMin < mColorRangeMax) {
        const int increment = (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000;
        const int to = std::min(mColorRangeMin + increment, mColorRangeMax);
        ColorizeRange(mColorRangeMin, to);
        mColorRangeMin = to;

        if (mColorRangeMax == mColorRangeMin) {
            mColorRangeMin = std::numeric_limits<int>::max();
            mColorRangeMax = 0;
        }
        return;
    }
}

float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const {
    auto& line = mLines[aFrom.mLine];
    float distance = 0.0f;
    float spaceSize =
        ImGui::GetFont()
            ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr)
            .x;
    int colIndex = GetCharacterIndex(aFrom);
    for (size_t it = 0u; it < line.size() && it < colIndex;) {
        if (line[it].mChar == '\t') {
            distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) *
                       (float(mTabSize) * spaceSize);
            ++it;
        } else {
            auto d = UTF8CharLength(line[it].mChar);
            char tempCString[7];
            int i = 0;
            for (; i < 6 && d-- > 0 && it < (int)line.size(); i++, it++)
                tempCString[i] = line[it].mChar;

            tempCString[i] = '\0';
            distance += ImGui::GetFont()
                            ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, tempCString,
                                            nullptr, nullptr)
                            .x;
        }
    }

    return distance;
}

void TextEditor::EnsureCursorVisible() {
    if (!mWithinRender) {
        mScrollToCursor = true;
        return;
    }

    float scrollX = ImGui::GetScrollX();
    float scrollY = ImGui::GetScrollY();

    auto height = ImGui::GetWindowHeight();
    auto width = ImGui::GetWindowWidth();

    auto top = 1 + (int)ceil(scrollY / mCharAdvance.y);
    auto bottom = (int)ceil((scrollY + height) / mCharAdvance.y);

    auto left = (int)ceil(scrollX / mCharAdvance.x);
    auto right = (int)ceil((scrollX + width) / mCharAdvance.x);

    auto pos = GetActualCursorCoordinates();
    auto len = TextDistanceToLineStart(pos);

    if (pos.mLine < top)
        ImGui::SetScrollY(std::max(0.0f, (pos.mLine - 1) * mCharAdvance.y));
    if (pos.mLine > bottom - 4)
        ImGui::SetScrollY(std::max(0.0f, (pos.mLine + 4) * mCharAdvance.y - height));
    if (len + mTextStart < left + 4)
        ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4));
    if (len + mTextStart > right - 4)
        ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width));
}

int TextEditor::GetPageSize() const {
    auto height = ImGui::GetWindowHeight() - 20.0f;
    return (int)floor(height / mCharAdvance.y);
}

TextEditor::UndoRecord::UndoRecord(
    const std::string& aAdded, const TextEditor::Coordinates aAddedStart,
    const TextEditor::Coordinates aAddedEnd, const std::string& aRemoved,
    const TextEditor::Coordinates aRemovedStart, const TextEditor::Coordinates aRemovedEnd,
    TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter)
    : mAdded(aAdded), mAddedStart(aAddedStart), mAddedEnd(aAddedEnd), mRemoved(aRemoved),
      mRemovedStart(aRemovedStart), mRemovedEnd(aRemovedEnd), mBefore(aBefore), mAfter(aAfter) {
    ASSERT(mAddedStart <= mAddedEnd);
    ASSERT(mRemovedStart <= mRemovedEnd);
}

void TextEditor::UndoRecord::Undo(TextEditor* aEditor) {
    if (!mAdded.empty()) {
        aEditor->DeleteRange(mAddedStart, mAddedEnd);
        aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2);
    }

    if (!mRemoved.empty()) {
        auto start = mRemovedStart;
        aEditor->InsertTextAt(start, mRemoved.c_str());
        aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2);
    }

    aEditor->mState = mBefore;
    aEditor->EnsureCursorVisible();
}

void TextEditor::UndoRecord::Redo(TextEditor* aEditor) {
    if (!mRemoved.empty()) {
        aEditor->DeleteRange(mRemovedStart, mRemovedEnd);
        aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1);
    }

    if (!mAdded.empty()) {
        auto start = mAddedStart;
        aEditor->InsertTextAt(start, mAdded.c_str());
        aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1);
    }

    aEditor->mState = mAfter;
    aEditor->EnsureCursorVisible();
}

const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() {
    static bool inited = false;
    static LanguageDefinition langDef;
    if (!inited) {
        static const char* const keywords[] = {
            "auto",           "break",        "case",     "char",     "const",      "continue",
            "default",        "do",           "double",   "else",     "enum",       "extern",
            "float",          "for",          "goto",     "if",       "inline",     "int",
            "long",           "register",     "restrict", "return",   "short",      "signed",
            "sizeof",         "static",       "struct",   "switch",   "typedef",    "union",
            "unsigned",       "void",         "volatile", "while",    "_Alignas",   "_Alignof",
            "_Atomic",        "_Bool",        "_Complex", "_Generic", "_Imaginary", "_Noreturn",
            "_Static_assert", "_Thread_local"};
        for (auto& k : keywords)
            langDef.mKeywords.insert(k);

        static const char* const identifiers[] = {
            "abort",   "abs",     "acos",    "asin",     "atan",    "atexit",  "atof",
            "atoi",    "atol",    "ceil",    "clock",    "cosh",    "ctime",   "div",
            "exit",    "fabs",    "floor",   "fmod",     "getchar", "getenv",  "isalnum",
            "isalpha", "isdigit", "isgraph", "ispunct",  "isspace", "isupper", "kbhit",
            "log10",   "log2",    "log",     "memcmp",   "modf",    "pow",     "putchar",
            "putenv",  "puts",    "rand",    "remove",   "rename",  "sinh",    "sqrt",
            "srand",   "strcat",  "strcmp",  "strerror", "time",    "tolower", "toupper"};
        for (auto& k : identifiers) {
            Identifier id;
            id.mDeclaration = "Built-in function";
            langDef.mIdentifiers.insert(std::make_pair(std::string(k), id));
        }

        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]",
            PaletteIndex::Punctuation));

        langDef.mCommentStart = "/*";
        langDef.mCommentEnd = "*/";
        langDef.mSingleLineComment = "//";

        langDef.mCaseSensitive = true;
        langDef.mAutoIndentation = true;

        langDef.mName = "GLSL";

        inited = true;
    }
    return langDef;
}

// Source: https://github.com/dfranx/ImGuiColorTextEdit/blob/master/TextEditor.cpp
const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SPIRV() {
    static bool inited = false;
    static LanguageDefinition langDef;
    if (!inited) {
        /*
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>("[ \\t]*#[
        \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string,
        PaletteIndex>("\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string,
        PaletteIndex>("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string,
        PaletteIndex>("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]",
        PaletteIndex::Punctuation));
        */

        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String));
        langDef.mTokenRegexStrings.push_back(
            std::make_pair<std::string, PaletteIndex>("[ =\\t]Op[a-zA-Z]*", PaletteIndex::Keyword));
        langDef.mTokenRegexStrings.push_back(
            std::make_pair<std::string, PaletteIndex>("%[_a-zA-Z0-9]*", PaletteIndex::Identifier));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number));
        langDef.mTokenRegexStrings.push_back(std::make_pair<std::string, PaletteIndex>(
            "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number));

        langDef.mCommentStart = "/*";
        langDef.mCommentEnd = "*/";
        langDef.mSingleLineComment = ";";

        langDef.mCaseSensitive = true;
        langDef.mAutoIndentation = false;

        langDef.mName = "SPIR-V";

        inited = true;
    }
    return langDef;
}

} // namespace Core::Devtools::Widget
